<?php
/**
 * Class for reading in NBT-format files.
 * Based on the work of Justin Martin <frozenfire@thefrozenfire.com>.
 * Altered to use BCMath rather than GMP, because BCMath comes by default with PHP >= 4.0.4, GMP never did.
 * 
 * @author  Julian Rupp <mail@nuclearflux.net>
 * @version 1.1
 *
 * Dependencies:
 *  PHP 4.3+ (5.3+ recommended)
 *  BCMath Extension
 */

extension_loaded("bcmath") or die("The NBT class requires the BCMath extension.");
class NBT {
	public $root = array();
	
	public $verbose = false;
	
	const TAG_END = 0;
	const TAG_BYTE = 1;
	const TAG_SHORT = 2;
	const TAG_INT = 3;
	const TAG_LONG = 4;
	const TAG_FLOAT = 5;
	const TAG_DOUBLE = 6;
	const TAG_BYTE_ARRAY = 7;
	const TAG_STRING = 8;
	const TAG_LIST = 9;
	const TAG_COMPOUND = 10;
	
	public function loadFile($filename) {
		$fp = fopen("compress.zlib://{$filename}", "rb");
		$this->traverseTag($fp, $this->root);
		return end($this->root);
	}
	
	public function writeFile($filename) {
		$fp = fopen("compress.zlib://{$filename}", "wb");
		foreach($this->root as $rootTag) if(!$this->writeTag($fp, $rootTag)) return false;
		return true;
	}
	
	public function purge() {
		$this->root = array();
	}
	
	protected function _signedlong2intstring($bin)
	{
		if(strlen($bin) != 8)
		{
			return(false);
		}
		list(,$firstHalf) = unpack("N", substr($bin,0,4));
		list(,$secondHalf) = unpack("N", substr($bin,4,4));
		$value = bcadd($secondHalf, bcmul($firstHalf, "4294967296"));
		if(bccomp($value, bcpow(2, 63)) >= 0) $value = bcsub($value, bcpow(2, 64));
		return($value);
	}
	
	protected function _intstring2signedlong($string)
	{
		#todo: input check

		$sbt = array();
		$sby = array();
		$ret = '';
		$c_remaining = $string;
		$c_mod = 0;

		for($i=0;$i<64;$i++)
		{
			$sbt[$i] = 0;
		}

		for($mp=63,$i=0;$mp>=0,$i<64;$mp--,$i++)
		{
			$c_mod = bcdiv($c_remaining,bcpow(2,$mp));
			$c_remaining = bcmod($c_remaining,bcpow(2,$mp));
			if(bccomp($c_mod,"1") >= 0)
			{
				$sbt[$i] = 1;
			}

			if(bccomp($c_remaining,"0") == 0)
			{
				break;
			}
		}

		for($i=0;$i<8;$i++)
		{
			$bin = '';
			for($j=0;$j<8;$j++)
			{
				$os = ($i*8)+$j;
				$bin .= $sbt[$os];
			}
			$sby[$i] = chr(bindec($bin));
		}
		$ret = implode($sby);
		if(bccomp($ret, bcpow(2, 63)) >= 0)
		{
			$ret = bcsub($ret, bcpow(2, 64));
		}

		return($ret);
	}
	
	protected function traverseTag($fp, &$tree) {
		if(feof($fp)) return false;
		$tagType = $this->readType($fp, self::TAG_BYTE); // Read type byte.
		if($tagType == self::TAG_END) {
			return false;
		} else {
			if($this->verbose) $position = ftell($fp);
			$tagName = $this->readType($fp, self::TAG_STRING);
			$tagData = $this->readType($fp, $tagType);
			if($this->verbose) echo "Reading tag \"{$tagName}\" at offset {$position}".PHP_EOL;
			$tree[] = array("type"=>$tagType, "name"=>$tagName, "value"=>$tagData);
			return true;
		}
	}
	
	protected function writeTag($fp, $tag) {
		if($this->verbose) echo "Writing tag \"{$tag["name"]}\" of type {$tag["type"]}".PHP_EOL;
		return $this->writeType($fp, self::TAG_BYTE, $tag["type"]) && $this->writeType($fp, self::TAG_STRING, $tag["name"]) && $this->writeType($fp, $tag["type"], $tag["value"]);
	}
	
	protected function readType($fp, $tagType) {
		switch($tagType) {
			case self::TAG_BYTE: // Signed byte (8 bit)
				list(,$unpacked) = unpack("c", fread($fp, 1));
				return $unpacked;
			case self::TAG_SHORT: // Signed short (16 bit, big endian)
				list(,$unpacked) = unpack("n", fread($fp, 2));
				if($unpacked >= pow(2, 15)) $unpacked -= pow(2, 16); // Convert unsigned short to signed short.
				return $unpacked;
			case self::TAG_INT: // Signed integer (32 bit, big endian)
				list(,$unpacked) = unpack("N", fread($fp, 4));
				if($unpacked >= pow(2, 31)) $unpacked -= pow(2, 32); // Convert unsigned int to signed int
				return $unpacked;
			case self::TAG_LONG: // Signed long (64 bit, big endian)
				return($this->_signedlong2intstring(fread($fp, 8)));
			case self::TAG_FLOAT: // Floating point value (32 bit, big endian, IEEE 754-2008)
				list(,$value) = (pack('d', 1) == "\77\360\0\0\0\0\0\0")?unpack('f', fread($fp, 4)):unpack('f', strrev(fread($fp, 4)));
				return $value;
			case self::TAG_DOUBLE: // Double value (64 bit, big endian, IEEE 754-2008)
				list(,$value) = (pack('d', 1) == "\77\360\0\0\0\0\0\0")?unpack('d', fread($fp, 8)):unpack('d', strrev(fread($fp, 8)));
				return $value;
			case self::TAG_BYTE_ARRAY: // Byte array
				$arrayLength = $this->readType($fp, self::TAG_INT);
				$array = array();
				for($i = 0; $i < $arrayLength; $i++) $array[] = $this->readType($fp, self::TAG_BYTE);
				return $array;
			case self::TAG_STRING: // String
				if(!$stringLength = $this->readType($fp, self::TAG_SHORT)) return "";
				$string = utf8_decode(fread($fp, $stringLength)); // Read in number of bytes specified by string length, and decode from utf8.
				return $string;
			case self::TAG_LIST: // List
				$tagID = $this->readType($fp, self::TAG_BYTE);
				$listLength = $this->readType($fp, self::TAG_INT);
				if($this->verbose) echo "Reading in list of {$listLength} tags of type {$tagID}.".PHP_EOL;
				$list = array("type"=>$tagID, "value"=>array());
				for($i = 0; $i < $listLength; $i++) {
					if(feof($fp)) break;
					$list["value"][] = $this->readType($fp, $tagID);
				}
				return $list;
			case self::TAG_COMPOUND: // Compound
				$tree = array();
				while($this->traverseTag($fp, $tree));
				return $tree;
		}
	}
	
	protected function writeType($fp, $tagType, $value) {
		switch($tagType) {
			case self::TAG_BYTE: // Signed byte (8 bit)
				return fwrite($fp, pack("c", $value));
			case self::TAG_SHORT: // Signed short (16 bit, big endian)
				if($value < 0) $value += pow(2, 16); // Convert signed short to unsigned short
				return fwrite($fp, pack("n", $value));
			case self::TAG_INT: // Signed integer (32 bit, big endian)
				if($value < 0) $value += pow(2, 32); // Convert signed int to unsigned int
				return fwrite($fp, pack("N", $value));
			case self::TAG_LONG: // Signed long (64 bit, big endian)
				$data = $this->_intstring2signedlong($value);
				return fwrite($fp, $data);
			case self::TAG_FLOAT: // Floating point value (32 bit, big endian, IEEE 754-2008)
				return fwrite($fp, (pack('d', 1) == "\77\360\0\0\0\0\0\0")?pack('f', $value):strrev(pack('f', $value)));
			case self::TAG_DOUBLE: // Double value (64 bit, big endian, IEEE 754-2008)
				return fwrite($fp, (pack('d', 1) == "\77\360\0\0\0\0\0\0")?pack('d', $value):strrev(pack('d', $value)));
			case self::TAG_BYTE_ARRAY: // Byte array
				return $this->writeType($fp, self::TAG_INT, count($value)) && fwrite($fp, call_user_func_array("pack", array_merge(array("c".count($value)), $value)));
			case self::TAG_STRING: // String
				$value = utf8_encode($value);
				return $this->writeType($fp, self::TAG_SHORT, strlen($value)) && fwrite($fp, $value);
			case self::TAG_LIST: // List
				if($this->verbose) echo "Writing list of ".count($value["value"])." tags of type {$value["type"]}.".PHP_EOL;
				if(!($this->writeType($fp, self::TAG_BYTE, $value["type"]) && $this->writeType($fp, self::TAG_INT, count($value["value"])))) return false;
				foreach($value["value"] as $listItem) if(!$this->writeType($fp, $value["type"], $listItem)) return false;
				return true;
			case self::TAG_COMPOUND: // Compound
				foreach($value as $listItem) if(!$this->writeTag($fp, $listItem)) return false;
				if(!fwrite($fp, "\0")) return false;
				return true;
		}
	}
}
?>